#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include "msfs.h"
#include "readin.h"

/* from readin.c */
extern enum command_type read_command(void);
extern char arg_buf[/* BUF_SIZE */];

int             image_fd = 0;           /* image file descriptor */
char            image_path[BUF_SIZE];   /* image file path */
unsigned int    bps;                    /* bytes per sector */
unsigned int    spc;                    /* number of sectors per cluster */
unsigned int    rsvd_num;               /* number of reserved sectors */
unsigned int    fat_num;                /* number of FAT tables */
unsigned int    re_num;                 /* number of root entries */
unsigned int    spf;                    /* number of sector per FAT table */

/*
 * convert first cluster number to byte offset
 * clusters are numbered consecutively, starting with
 * cluster number 2, which lies at the begining of the
 * data area.
 * 0, 1 are reserved.
 */
#define CTBO(cn) (((cn - 2)*spc + rsvd_num + fat_num*spf + re_num/(bps/32)) * bps)

#define WD_DEPTH    256                 /* maximum depth of working directory */
struct wde_struct {
    char            fnm[8];
    unsigned int    bof;
};
struct wde_struct   cwd_queue[WD_DEPTH];
unsigned int        cwd_p = 1;

void fd_init(void)
{
    cwd_p = 1;
    memset(&cwd_queue[0], 0, sizeof(struct wde_struct));
}

int fd_load(char path[])
{
    int rval;

    if (image_fd != STDIN_FILENO) {
        close(image_fd);
    }
    if (-1 == (image_fd = open(path, O_RDWR))) {
        fprintf(stderr, "open %s for reading failed, please check.\n", path);
        image_fd = STDIN_FILENO;
        return -1;
    }
    fd_init();
    /* read bytes per sector from boot sector offset 0x0b of size 2 byte */
    lseek(image_fd, 0x0b, SEEK_SET);
    rval = -2;
    if (read(image_fd, (void *)&bps, 2) <= 0) {
        fprintf(stderr, "read \'bytes per sector\' failed.\n");
        goto err_out;
    }
#ifdef DEBUG
    fprintf(stderr, DEBUG "bytes per sector: %d\n", bps);
#endif
    /*
     * read number of sectors per cluster, reversed sectors,
     * FAT tables and sectors per FAT table
     */
    lseek(image_fd, 0x0d, SEEK_SET);
    rval = -3;
    if (read(image_fd, (void *)&spc, 1) <= 0) {
        fprintf(stderr, "read \'number of sectors per cluster\' failed.\n");
        goto err_out;
    }
    lseek(image_fd, 0x0e, SEEK_SET);
    rval = -4;
    if (read(image_fd, (void *)&rsvd_num, 2) <= 0) {
        fprintf(stderr, "read \'number of reserved sectors\' failed.\n");
        goto err_out;
    }
    lseek(image_fd, 0x10, SEEK_SET);
    rval = -5;
    if (read(image_fd, (void *)&fat_num, 1) <= 0) {
        fprintf(stderr, "read \'number of FAT tables\' failed.\n");
        goto err_out;
    }
    lseek(image_fd, 0x11, SEEK_SET);
    rval = -6;
    if (read(image_fd, (void *)&re_num, 2) <= 0) {
        fprintf(stderr, "read \'number of root directory entries\' failed.\n");
        goto err_out;
    }
    lseek(image_fd, 0x16, SEEK_SET);
    rval = -7;
    if (read(image_fd, (void *)&spf, 2) <= 0) {
        fprintf(stderr, "read \'sectors per FAT table\' failed.\n");
        goto err_out;
    }
#ifdef DEBUG
    fprintf(stderr, DEBUG "number of sectors per cluster: %d\n", spc);
    fprintf(stderr, DEBUG "number of reserved sector: %d\n", rsvd_num);
    fprintf(stderr, DEBUG "number of FAT tables: %d\n", fat_num);
    fprintf(stderr, DEBUG "number of root directory entries: %d\n", re_num);
    fprintf(stderr, DEBUG "sectors per FAT table: %d\n", spf);
#endif
    /* calculate the root directory's byte offset */
    cwd_p = 0;
    cwd_queue[cwd_p].bof = (rsvd_num + spf * fat_num) * bps;
    cwd_queue[cwd_p].fnm[0] = '\0';
    cwd_p++;
#ifdef DEBUG
    fprintf(stderr, DEBUG "byte offset of root directory: %d\n", cwd_queue[0].bof);
#endif
    return 0;
err_out:
    close(image_fd);
    return rval;
}

void print_attrib(char atb, char msk, int d)
{
    if (atb & msk) {
        fputc(d, stdout);
    }
    else {
        fputc('-', stdout);
    }
}

void extract_date(struct date *d, char s[/* 2 */])
{
    EXTRACT_YEAR((*d).year, s);
    EXTRACT_MONTH((*d).month, s);
    EXTRACT_DAY((*d).day, s);
}

void extract_time(struct time *d, char s[/* 2 */])
{
#ifdef DEBUG
    fprintf(stderr, DEBUG "dump time: 0x%x%x\n", s[1], s[0]);
#endif
    EXTRACT_HOUR((*d).hour, s);
    EXTRACT_MINIUTE((*d).miniute, s);
    EXTRACT_SEC((*d).second, s);
}

int fd_ls(void)
{
    struct dir_entry    de;
    unsigned int        cnt = 0;
    struct date         d;
    struct time         t;
    int                 i;

    if (image_fd == 0) {
        fprintf(stderr, "no ms-dos partition was loaded.\n");
        return -1;
    }
    lseek(image_fd, cwd_queue[cwd_p-1].bof, SEEK_SET);
    do {
        read(image_fd, (void *)&de, DIR_ENTRY_SIZE);
        cnt++;
        switch ((unsigned char)de.fnm[0]) {
            case FBFN_UNUSED:
            case FBFN_DELETED:          /* lies, ;-) */
            case FBFN_NEXT_VALID:
                break;
            default:
                /* print attribute */
                print_attrib(de.atb, DEA_DIR, 'd');
                print_attrib(de.atb, DEA_SYS, 's');
                print_attrib(de.atb, DEA_HID, 'h');
                print_attrib(de.atb, DEA_RDO, 'r');
                fputc(' ', stdout);
                /* print size */
                fprintf(stdout, "%10d ", de.size);
                /* date and time of last modification */
                extract_date(&d, de.mdate);
                extract_time(&t, de.mtime);
                fprintf(stdout, "%04d-%02d-%02d ", d.year, d.month, d.day);
                fprintf(stdout, "%02d:%02d:%02d ", t.hour, t.miniute, t.second);
                /* date and time of creation */
                extract_date(&d, de.cdate);
                extract_time(&t, de.ctime);
                fprintf(stdout, "%04d-%02d-%02d ", d.year, d.month, d.day);
                fprintf(stdout, "%02d:%02d:%02d ", t.hour, t.miniute, t.second);
                /* print filename and extention if any */
                for (i = 0; de.fnm[i] != 0x20 && i <= 8; i++) {
                    fputc(de.fnm[i], stdout);
                }
                if (de.ext[0] != 0x20) {
                    fputc('.', stdout);
                    for (i = 0; de.fnm[i] != 0x20 && i <= 3; i++) {
                        fputc(de.ext[i], stdout);
                    }
                }
                fputc('\n', stdout);
        }
    } while (de.fnm[0] != FBFN_UNUSED && cnt < re_num);
    return 0;
}

/*
 * paramenters bbuf and count are carriers of return value
 * buf would be freed by the caller
 */
static int fd_check_dir_path(char *dir_path, struct wde_struct **bbuf, int *count)
{
    struct wde_struct   *buf;
    struct wde_struct   *ep;            /* entry pointer */
    unsigned int        cnt;
    struct dir_entry    de;
    char                *p, *q;         /* point to dir_path */
    char                *rb;            /* point to right boundary of dir_path */
    char                fnm[8];         /* strncmp(fnm, de.fnm, 8) */
    size_t              len;            /* strlen */
    int                 rval;

    /* uppercase the path */
    p = dir_path;
    while (*p) {
        *p = toupper(*p);
        p++;
    }
    rb = p;

    buf = (struct wde_struct *)calloc(WD_DEPTH, sizeof(struct wde_struct));
    if (dir_path[0] == '/') {
        memcpy(buf, cwd_queue, sizeof(struct wde_struct));
        cnt = 1;
        p = dir_path + 1;
    }
    else {
        memcpy(buf, cwd_queue, sizeof(struct wde_struct) * cwd_p);
        cnt = cwd_p;
        p = dir_path;
    }
    ep = buf;
    do {
        if (NULL != (q = strchr(p, '/'))) {
            len = q - p;
            if (len == 0) {
                p += 1;
                continue;
            }
        }
        else {
#ifdef DEBUG
            fprintf(stderr, "dir_path in fd_check_dir_path: %s\n", p);
#endif
            len = strlen(p);
            if (len == 0) {
                break;
            }
        }
        /* search for 'p' in 'ep' */
        lseek(image_fd, ep[cnt-1].bof, SEEK_SET);
        rval = -1;
        do {
            read(image_fd, &de, DIR_ENTRY_SIZE);
            switch ((unsigned char)de.fnm[0]) {
                case FBFN_UNUSED:
                case FBFN_DELETED:          /* lies, ;-) */
                case FBFN_NEXT_VALID:
                    break;
                default:
                    memset(fnm, '\x20', 8);
                    memcpy(fnm, p, len);
                    if (memcmp(fnm, DOT_CUR_DIR, 8) == 0) {
                        rval = 0;
                    }
                    else if (memcmp(fnm, DOT_PAR_DIR, 8) == 0) {
                        /* already at the root directory */
                        if (cnt <= 1) {
                            rval = -2;
                        }
                        else {
                            cnt--;
                            rval = 0;
                        }
                    }
                    else if (memcmp(fnm, de.fnm, 8) == 0) {
                        if (!(de.atb & DEA_DIR)) {
                            rval = -3;
                            break;
                        }
                        memcpy(ep[cnt].fnm, fnm, len);
                        ep[cnt].bof = CTBO(de.scn);
                        cnt++;
                        rval = 0;           /* break inner loop */
                    }
            }
        } while (rval == -1 && de.fnm[0] != FBFN_UNUSED);
        if (rval < 0) {
            break;
        }
        p += (len + 1);
        /* go beyond now */
        if (p >= rb) {
            break;
        }
    } while (1);

    (*count) = cnt;
    (*bbuf) = buf;
    return rval;
}

void fd_switch_check_dir(int rval, char *dir_path)
{
    switch (rval) {
        case -1:
            fprintf(stderr, "cannot find directory: %s.\n", dir_path);
            break;
        case -2:
            fprintf(stderr, "already at the root directory.\n");
            break;
        case -3:
            fprintf(stderr, "%s is not leading to a directory.\n", dir_path);
            break;
    }
}

int fd_cd(char *dir_path)
{
    struct wde_struct   *buf;           /* for cd /... */
    int                 rval;
    int                 cnt;

    rval = fd_check_dir_path(dir_path, &buf, &cnt);
    fd_switch_check_dir(rval, dir_path);
    if (rval < 0) {
        return rval;
    }

    memcpy(cwd_queue, buf, sizeof(struct wde_struct) * cnt);
    cwd_p = cnt;
    free(buf);
    return rval;
}

int fd_rm(char *file_path)
{
    struct wde_struct   *buf;           /* for cd /... */
    int                 rval;
    int                 cnt;
    char                *p, *q;         /* point to the last '/' in file_path */
    char                fnm[8];         /* dump p to the format in a directory */
    char                ext[3];
    int                 flag;
    struct dir_entry    de;

#ifdef DEBUG
    fprintf(stderr, "rm %sEOL\n", file_path);
#endif
    if (NULL != (p = strrchr(file_path, '/'))) {
        *p = '\0';
        /* now file_path is actually a directory path */
        rval = fd_check_dir_path(file_path, &buf, &cnt);
        fd_switch_check_dir(rval, file_path);
        if (rval < 0) {
            return rval;
        }

        if (*(p+1) == '\0') {
            *p = '/';
            fprintf(stderr, "%s is a directory.\n", file_path);
            rval = -4;
            goto clean_out;
        }
        p++;
        flag = 0;
    }
    else {
        p = file_path;
        flag = 1;
    }
    /* set the return value */
    rval = -5;

    /* uppercase the filename */
    q = p;
    while (*p) {
        *p = toupper(*p);
        p++;
    }
    p = q;
    q = strrchr(p, '.');
    /*
     * now p point to the uppercased filename
     * q point to the character before extention.
     */
    memset(fnm, ' ', 8);
    memcpy(fnm, p, q == NULL ? strlen(p) : q-p);
    memset(ext, ' ', 3);
    if (q != NULL) {
        q++;
        memcpy(ext, q, strlen(q));
    }
    /* position to the appropriate point */
    if (flag == 0) {
        lseek(image_fd, buf[cnt-1].bof, SEEK_SET);
    }
    else {
        lseek(image_fd, cwd_queue[cwd_p-1].bof, SEEK_SET);
    }
#ifdef DEBUG
    write(STDERR_FILENO, fnm, 8);
    write(STDERR_FILENO, "\n", 1);
    write(STDERR_FILENO, ext, 3);
    write(STDERR_FILENO, "\n", 1);
#endif
    do {
        read(image_fd, &de, DIR_ENTRY_SIZE);
        switch ((unsigned char)de.fnm[0]) {
            case FBFN_UNUSED:
            case FBFN_DELETED:          /* lies, ;-) */
            case FBFN_NEXT_VALID:
                break;
            default:
                if (memcmp(fnm, de.fnm, 8) == 0 && memcmp(ext, de.ext, 3) == 0) {
                    if (de.atb & DEA_DIR) {
                        fprintf(stderr, "%s is a directory.\n", p);
                        goto clean_out;
                    }
                    lseek(image_fd, -32, SEEK_CUR);
                    write(image_fd, "\xe5", 1);
                    rval = 0;
                    goto out;
                }
        }
    } while (de.fnm[0] != FBFN_UNUSED);

out:
    if (rval < 0) {
        fprintf(stderr, "cannot find file %s\n", p);
    }
clean_out:
    if (flag == 0) {
        free(buf);
    }
#ifdef DEBUG
    fprintf(stderr, DEBUG "rval in fd_rm: %d\n", rval);
#endif
    return rval;
}

void print_prompt(void)
{
    int i;
    for (i = 0; i < cwd_p && i < WD_DEPTH; i++) {
        printf("%s", cwd_queue[i].fnm);
        printf("/");
        if (i == cwd_p - 1) {
            printf(" $ ");
            return;
        }
    }
}

/*
enum command_type {
    CMD_LOAD, CMD_LS, CMD_CD, CMD_RM, CMD_EXIT, CMD_UNVALID,
};
*/

int main(int argc, char *argv[])
{
    enum command_type type;

    while (1) {
        print_prompt();
        type = read_command();
        if (type != CMD_LOAD && type != CMD_UNVALID && type != CMD_EXIT && image_fd == STDIN_FILENO) {
            fprintf(stderr, "please load the image before any operation.\n");
            continue;
        }
        switch (type) {
            case CMD_LOAD:
                strncpy(image_path, arg_buf, BUF_SIZE);
                fd_load(image_path);
                break;
            case CMD_LS:
                fd_ls();
                break;
            case CMD_CD:
                fd_cd(arg_buf);
                break;
            case CMD_RM:
                fd_rm(arg_buf);
                break;
            case CMD_EXIT:
                close(image_fd);
                exit(EXIT_SUCCESS);
            default: /* CMD_UNVALID */
                break;
        }
    }
    return 0;
}

